Java-Note-NIO
2018-04-14
Java
- 核心部分
- Channels
- Buffers
- Selectors
- 其它部分不过是一些工具类而已
- Channel
Buffer: 用于和Channel进行交互. 数据是从通道读入缓冲区, 从缓冲区写入通道.
- 本质: 缓冲区本质上是一块可以写入数据, 然后可以从中读取数据的内存. 这块内存被包装成NIO Buffer对象, 并提供一组方法, 用来方便的访问该块内存
- 主要的Buffer实现
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
- 当向buffer写入数据时, buffer会记录写下了多少数据. 一旦要读取数据, 需要通过flip()方法将Buffer从写模式切换到读模式. 在读模式下, 可以赌气之前写入到buffer的所有数据
- 一旦读完了所有数据, 就需要清空缓冲区, 让它可以再次被写入. 有两种方式能请清空缓冲区: 调用clear()/compact()方法. clear()方法会清空整个缓冲区, compact()方法只会清除已经读过的数据, 任何未读的数据都被移到缓冲区的起始处, 新写入的数据将放到缓冲区未读数据的后面
- Buffer的三个属性
- capacity: 作为一个内存块, Buffer有一个固定的大小值,
你只能往里写capacity个byte, long, char等类型. 一旦Buffer满了, 需要将其清空(通过读数据或清除数据)才能继续往里写数据 - position:
- 写数据: position表示当前的位置. 最大可为capacity-1
- 读数据: 当Buffer从写模式切换到读模式, position会被重置为0. 当从Buffer的position处读取数据时, position向前移动到下一个可读的位置
- limit:
- 写模式: limit == capacity
- 读模式: limit == 写数据时的position值
- capacity: 作为一个内存块, Buffer有一个固定的大小值,
- Buffer的分配: 想获得一个Buffer对象首先要进行分配. 每一个Buffer类都有一个allocate方法.
- 例子
- 分配48个字节capacity的ByteBuffer的例子:
ByteBuffer buf = ByteBuffer.allocate(48) - 分配1024个字符的CharBuffer的例子:
CharBuffer buf = CharBuffer.allocate(1024)
- 分配48个字节capacity的ByteBuffer的例子:
- 例子
- 向Buffer中写数据
- 方式
- 从Channel写到Buffer
- 通过Buffer的put()方法写到Buffer里(可以自由操纵每一个单位)
- 方式
- flip()方法: flip方法将Buffer从写模式切换到读模式. 调用flip()方法会将position设为0, 并将limit设置成之前position的值
- 从Buffer中读取数据
- 方式
- 从Buffer读取数据到Channel
- 例子:
int bytesWritten = inChannel.write(buf)
- 例子:
- 使用get()方法从Buffer中读取数据
- 例子:
byte aByte = buf.get()
- 例子:
- 从Buffer读取数据到Channel
- 方式
- rewind()方法: Buffer.rewind()将position设为0, 使用户可以重读Buffer中的所有数据. limit保持不变, 仍然表示能从Buffer中读取多少个元素
- clear()与compact()方法: 一旦读完Buffer中的数据, 需要让Buffer准备好再次被写入. 可以通过clear()或compact()方法来完成
- clear(): position将被设为0, limit被设置为capacity的值. 换句话说, Buffer被清空了. Buffer中的数据并未清除, 只是这些标记告诉我们可以从哪里开始往Buffer里写数据. 如果Buffer中有未读的数据, 调用clear()方法, 数据将被遗忘, 意味着不再有任何标记会告诉你哪些数据被读过, 哪些还没有. 如果Buffer中仍有未读的数据, 且后续还需要这些数据, 但是此时想要先写些数据, 那么使用compact()方法
- compact(): 将所有未读的数据拷贝到Buffer起始处. 然后将position设到最后一个未读元素正后面. limit属性依然像clear()方法一样, 设置成capacity. 现在Buffer准备好写数据了, 但是不会覆盖未读的数据.
mark()与reset()方法: 通过调用Buffer.mark()方法, 可以标记Buffer中的一个特定position. 之后可以通过调用Buffer.reset()方法恢复到这个position.
- 例子:1
2
3buffer.mark();
// call buffer.get() a couple of times, e.g. during parsing
buffer.reset();equals()与compareTo()方法
- equals(): 当满足下列条件时, 表示两个Buffer相等:
- 有相同的类型(byte/char/int等)
- Buffer中剩余的byte/char等的个数相等
- Buffer中所有剩余的byte/char相同
- 注: 剩余元素是从position到limit之间的元素
- 如上所言, equals只是比较Buffer的一部分, 不是每一个在它里面的元素都比较. 实际上, 它只比较Buffer中的剩余元素
- compareTo()方法: 比较两个Buffer的剩余元素(byte/char等), 如果满足下列条件, 则认为一个Buffer”小于”另一个Buffer
- 第一个不相等的元素小于另一个Buffer中对应的元素
- 所有元素都相等, 但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)
- equals(): 当满足下列条件时, 表示两个Buffer相等:
Selector
- Selector允许单线程处理多个Channel. 如果应用打开了多个连接(通道), 但每个连接的流量都很低, 使用Selector就会很方便. 例如, 在一个聊天服务器中
- 为什么使用Selector: 仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
使用Selector:
创建一个Selector:
1
Selector selector = Selector.open();
向Selector注册Channel
1
2channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);与Selector一起使用时, Channel必须处于非阻塞模式. 这意味着不能将FileChannel与Selector一起使用, 因为FileChannel不能切换到非阻塞模式. 而套接字通道可以
注意register()方法的第二个参数. 这是一个”interest集合”, 意思是在通过Selector监听Channel时对什么事件感兴趣. 可以监听四种不同类型的事件:
- Connect
- Accept
- Read
- Write
- 这四种事件用SelectionKey的四个常量来表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- 如果对不止一种事件感兴趣, 那么可以用”位或”操作符将常量连接起来, 如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
SelectionKey:
当向Selector注册Channel时, register()方法会返回一个SelectionKey对象. 这个对象包含了一些你感兴趣的属性:
interest集合: 所选择的感兴趣的事件集合. 可以通过SelectionKey读写interest集合, 像这样
1
2
3
4
5int interestSet = selectionKey.interestOps();
boolean isInterstedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;可以看出, 用”位与”操作interest集合和给定的SelectionKey常量, 可以确定某个确定的事件是否在interest集合中
ready集合: 已准备就绪的通道的集合(interest集合中的), 可以这样访问ready集合
1
int readySet = selectionKey.readyOps();
检测interest集合可以使用检测interest集合一样的方法
1
2
3
4selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();Channel
- Selector
- 附加的对象(可选)
当把一个channel注册到指定的Selector上时, 实际上就是将(channel, selector)封装成一个SelectionKey对象, 并将此对象保存在Selector对象中
- 调用它的select()方法, 这个方法会一直阻塞到某个注册的通道有事件就绪. 一旦这个方法返回, 线程就可以处理这些事件, 事件的例子有如新连接进来/数据接收
- 调用select()方法, 会返回所感兴趣的事件(如连接/接受/读或写)已经准备就绪的那些通道.
- 重载的select()方法
- int select(): 保持阻塞知道至少有一个通道在注册的事件上就绪, 返回的int表示注册数
- int select(long timeout): 和select()一样, 除了最长会阻塞timeout毫秒
- int selectNow(): 不会阻塞, 不管什么通道立即返回, 如果没有就绪, 则返回0
- selectedKeys()
- 如果调用select()方法后返回的值大于1, 就可以通过调用selector的selectedKeys()方法, 访问”已选择键集(selected key set)”中的就绪通道. 例如:
Set selectedKeys = selector.selectedKeys(), 可以遍历这个已选择的键的集合来访问就绪的通道
- 如果调用select()方法后返回的值大于1, 就可以通过调用selector的selectedKeys()方法, 访问”已选择键集(selected key set)”中的就绪通道. 例如: